#!/usr/bin/env python2
'''
HTTP/WebSocket server serving to control the Mars rover.

Serves static content from http directory, and websocket connection
on /mars.
'''
import datetime
import socket

import tornado.ioloop
import tornado.web
import tornado.websocket

MESSAGES = {
    u'connected': {
        'en': u'Connected to Mars rover.',
        'fi': u'Yhteys Mars-m\xf6nkij\xe4\xe4n luotu.',
    },
    u'connection lost': {
        'en': u'Connection to Mars was lost, reconnecting...',
        'fi': u'Yhteys Marsiin katkesi, korjataan...',
    },
    u'connection failed': {
        'en': u'Connecting to Mars failed, retrying...',
        'fi': u'Yhteydenotto Marsiin ei onnistunut, kokeillaan uudestaan...',
    },
    u'chosen': {
        'en': u'You are now in control of the Mars rover.',
        'fi': u'Olet nyt Mars-m\xf6nkij\xe4n ohjaaja.',
    },
    u'not chosen': {
        'en': u'Someone else is currently controlling the Mars rover.',
        'fi': u'Joku muu on t\xe4ll\xe4 hetkell\xe4 ohjaamassa Mars-m\xf6nkij\xe4\xe4.',
    },
    u'protocol error': {
        'en': u'Protocol error.',
        'fi': u'Protokollavirhe.',
    },
}

class WebSocketClientManager(object):
    '''
    Manages connections from multiple clients to prevent two clients from
    trying to control the Mars rover at the same time.
    '''
    def __init__(self):
        self._clients = set()
        self._admins = set()
        self.current = None
    
    def register_client(self, client):
        '''
        New client connected.
        '''
        self._clients.add(client)
    
    def request_control(self, client):
        '''
        Client requesting control of the Mars rover.
        '''
        if self.current is None:
            self.current = client
            self.unicast(u'chosen')
        else:
            self.unicast(u'not chosen', client)
        if not raspberry.active:
            raspberry.connect()
    
    def unregister_client(self, client):
        '''
        Some client disconnected.
        '''
        self._clients.discard(client)
        self._admins.discard(client)
        if client is self.current:
            self.change_control()
    
    def change_control(self, client=None):
        '''
        Change which client is in control.
        '''
        if client:
            self.current = client
        elif len(self._clients) == 0:
            self.current = None
        elif len(self._clients) == 1 or self.current is None:
            self.current = iter(self._clients).next()
        else:
            chosen = None
            choose = False
            for client in self._clients:
                if client is self.current:
                    choose = True
                elif choose:
                    chosen = client
                    break
            self.current = chosen or iter(self._clients).next()
        self.unicast(u'chosen')
    
    def broadcast(self, key):
        '''
        Broadcast a text message to all connected clients.
        '''
        text = MESSAGES.get(key, {'en': key})
        for client in self._clients|self._admins:
            client.write_message(
                u'TEXT:%s:%s' % (key, text.get(client.language))
            )
    
    def unicast(self, key, client=None):
        '''
        Send a text message to specific client.
        '''
        if client is None:
            client = self.current
        if client is not None:
            text = MESSAGES.get(key, {'en': key})
            client.write_message(
                u'TEXT:%s:%s' % (key, text.get(client.language))
            )
    
    def send_all(self, data):
        '''
        Send data to all connected clients.
        '''
        for client in self._clients|self._admins:
            client.write_message(data.decode('utf-8'))

    def send(self, data, client=None):
        '''
        Send data to specific client.
        '''
        if client is None:
            client = self.current
        if client is not None:
            client.write_message(data.decode('utf-8'))


ws = WebSocketClientManager()

class RPiConnection(object):
    '''
    Direct socket connection to Raspberry Pi.
    '''
    RPI_ADDRESS = ('192.168.1.3', 7000)
    RESTORE_TIME = 1 # time in seconds to try reconnecting after error
    RECONNECT_TIME = 10 # time in seconds to try reconnecting if can't connect
    
    def __init__(self, ioloop):
        self.socket = None
        self.stream = None
        self.active = False
        self.ioloop = ioloop
    
    def connect(self):
        '''
        Attempt to connect to Raspberry Pi.
        '''
        if self.socket is not None:
            self.close()
        self.socket = socket.socket()
        try:
            self.socket.connect(self.RPI_ADDRESS)
        except socket.error:
            self.close()
            ws.broadcast(u'connection failed')
            self.reconnect(self.RECONNECT_TIME)
        else:
            # register the connection with ioloop
            self.active = True
            ws.broadcast(u'connected')
            self.stream = tornado.iostream.IOStream(self.socket,
                io_loop=self.ioloop)
            self.stream.set_close_callback(self._stream_closed)
            self.stream.read_until('\n', self._message) # start reading
    
    def reconnect(self, reconnect_time):
        '''
        Schedule a reconnection.
        '''
        self.active = True
        when = datetime.timedelta(seconds=reconnect_time)
        self.ioloop.add_timeout(when, self.connect)
    
    def _stream_closed(self):
        '''
        Stream was closed for some reason.
        '''
        ws.broadcast(u'connection lost')
        self.stream = None
        if self.socket is not None:
            self.close()
        self.reconnect(self.RESTORE_TIME)
            
    def close(self):
        '''
        Close connection.
        '''
        if self.stream is not None:
            self.socket = None
            self.stream.close()
        elif self.socket is not None:
            self.socket.close()
            self.socket = None
        self.active = False
    
    def _message(self, message):
        '''
        Callback from IOStream, finished reading a message from RPi.
        '''
        ws.send_all(message)
        self.stream.read_until('\n', self._message) # wait for new message
    
    def send(self, message):
        '''
        Send a message to RPi.
        '''
        if self.stream is not None:
            self.stream.write(message)


class MarsWebSocket(tornado.websocket.WebSocketHandler):
    '''
    WebSocket handler to relay communications between RPi and web browser.
    '''
    def open(self):
        self.language = 'en'
        ws.register_client(self)

    def on_message(self, message):
        params = message.split(u':')
        if params[0] == u'set':
            if len(params) != 3:
                ws.unicast(u'protocol error', self)
            elif params[1] == u'language':
                self.language = params[2]
            else:
                ws.unicast(u'protocol error', self)
        elif params[0] == u'control':
            ws.request_control(self)
        elif params[0] == u'send':
            if ws.current is self:
                raspberry.send(params[1].encode('utf-8')+'\n')
            else:
                print "WS client attempted control of rover, but not authorized"
    
    def on_close(self):
        ws.unregister_client(self)


class StaticFileHandler(tornado.web.StaticFileHandler):
    def get_cache_time(self, path, modified, mime_type):
        return 0

application = tornado.web.Application([
    (r'/mars', MarsWebSocket),
    (r'/(.*)', StaticFileHandler, {'path': 'http'}),
])

if __name__ == '__main__':
    ioloop = tornado.ioloop.IOLoop.instance()
    application.listen(8888, io_loop=ioloop)
    raspberry = RPiConnection(ioloop)
    tornado.ioloop.IOLoop.instance().start()

